#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" Defines the ConfigFile class, a small but complete wrapper around ConfigParser.

The base class implementation isn't very useful, so we redefine it to
shorten the usage pattern of immediatly loading a file., e.g.:

    >>> c = ConfigFile()
    >>> c.read('tests/test-site/config.ini')

Becomes:

    >>> c = ConfigFile('tests/test-site/config.ini')

"""
from functools import partial
from ConfigParser import SafeConfigParser, NoOptionError

from tack.lib.jsontools import json_dumps, json_loads, supported_types


marker = object()


def testini():
    from cStringIO import StringIO    
    return StringIO("""
[DEFAULT]
a = 1
b = 2
spam = json:[0, 1, 2]
""")


class ConfigFile(SafeConfigParser):
    ## we prefix non-string values with this string to mark them as
    ## json encoded
    json_prefix = 'json:'


    def __init__(self, filename=None, section='DEFAULT'):
        """

        >>> c = ConfigFile(testini())
        >>> c
        {'a': '1', 'b': '2', 'spam': [0, 1, 2]}

        """
        SafeConfigParser.__init__(self)
        self.section = section
        self.filename = None
        if filename:
            self.filename = filename
            if hasattr(filename, 'readlines'):
                self.readfp(filename)
            else:
                self.read(filename)

    ## functions to get json encoded data in and out of this
    ## config "file".

    def decode(self, value):
        """

        >>> c = ConfigFile()
        >>> c.decode('3')
        '3'

        >>> c.decode('json:[4, 5]')
        [4, 5]

        >>> type(c.decode('json:{"1": 2, "0": -1}'))
        <type 'dict'>

        >>> type(c.decode('json:[1, 2]'))
        <type 'list'>

        >>> type(c.decode('json:null'))
        <type 'NoneType'>
        """
        if value.startswith(self.json_prefix):
            return json_loads(value[len(self.json_prefix):])
        return value

    def getdecoded(self, option, default=marker):
        """

        >>> c = ConfigFile(testini())
        >>> c.getdecoded('a')
        '1'

        >>> c.getdecoded('spam')
        [0, 1, 2]

        >>> c.getdecoded('eggs')
        Traceback (most recent call last):
        ...
        KeyError: 'eggs'

        """
        try:
            value = self.get(self.section, option)
        except (NoOptionError, ):
            if default is not marker:
                return default
            else:
                raise KeyError(option)
        return self.decode(value)

    def read(self, filename):
        """

        This isn't a real test, but it will have to do.

        >>> c = ConfigFile()
        >>> c.read
        <bound method ConfigFile.read ...>

        """
        response = SafeConfigParser.read(self, filename)
        if response not in (filename, [filename]):
            raise IOError('Not all configuration files read.')

    def set(self, option, value):
        """

        >>> l = [0, 1, 2]
        >>> c = ConfigFile()
        >>> c.set('test', l)
        >>> c['test']
        [0, 1, 2]
        """
        if any(isinstance(value, cls) for cls in supported_types):
            value = self.json_prefix + json_dumps(value)
        SafeConfigParser.set(self, self.section, option, value)

    def save(self, filename):
        self.write(open(filename, 'w'))

    def asdict(self):
        return dict(self.items())

    def __str__(self):
        return str(self.asdict())
    __repr__ = __str__

    ## walk and talk like a dictionary.  doc strings copied from
    ## corresponding dict method doc string.

    def __getitem__(self, key):
        """ x.__getitem__(y) <==> x[y]

        >>> c = ConfigFile()
        >>> c.readfp(testini())
        >>> c['a']
        '1'
        >>> c['spam']
        [0, 1, 2]

        """
        try:
            return self.getdecoded(key)
        except (NoOptionError, ):
            try:
                return self.get(self.section, key)
            except (NoOptionError, ):
                raise KeyError(key)

    def __setitem__(self, key, value):
        """ x.__setitem__(i, y) <==> x[i]=y

        >>> c = ConfigFile()
        >>> c['eggs'] = 'asdf'
        >>> c['eggs']
        'asdf'

        >>> c['ham'] = {}
        >>> c['ham']
        {}

        """
        self.set(key, value)

    def __delitem__(self, key):
        """ x.__delitem__(y) <==> del x[y]

        >>> c = ConfigFile(testini())
        >>> del(c['spam'])

        >>> c['spam']
        Traceback (most recent call last):
        ...
        KeyError: 'spam'
        """
        self.remove_option(self.section, key)

    def items(self):
        """ D.items() -> list of D's (key, value) pairs, as 2-tuples

        >>> c = ConfigFile()
        >>> c.items()
        []

        >>> c = ConfigFile(testini())
        >>> c.items()
        [('a', '1'), ('b', '2'), ('spam', [0, 1, 2])]
        """
        items = SafeConfigParser.items(self, self.section)
        return [(k, self.decode(v)) for k, v in items]

    def iteritems(self):
        """ D.iteritems() -> an iterator over the (key, value) items of D

        >>> c = ConfigFile(testini())
        >>> for k, v in c.iteritems():
        ...     print k, v
        a 1
        b 2
        spam [0, 1, 2]
        """
        return iter(self.items())

    def keys(self):
        """ D.keys() -> list of D's keys

        >>> c = ConfigFile(testini())
        >>> c.keys()
        ['a', 'b', 'spam']
        """
        return [k for k, v in self.items()]

    def iterkeys(self):
        """ D.iterkeys() -> an iterator over the keys of D

        >>> c = ConfigFile(testini())
        >>> for k in c.iterkeys():
        ...     print k
        a
        b
        spam
        """
        return iter(self.keys())

    __iter__ = iterkeys

    def values(self):
        """ D.values() -> list of D's values

        >>> c = ConfigFile(testini())
        >>> c.values()
        ['1', '2', [0, 1, 2]]
        """
        return [v for k, v in self.items()]

    def itervalues(self):
        """ D.itervalues() -> an iterator over the values of D

        >>> c = ConfigFile(testini())
        >>> for v in c.itervalues():
        ...     print v
        1
        2
        [0, 1, 2]
        """
        return iter(self.values())

    def copy(self):
        """ D.copy() -> a (not so) shallow copy of D

        The copy isn't as shallow as a dict copy because values are
        decoded from their string representations when called, not
        when read.

        >>> c = ConfigFile(testini())
        >>> n = c.copy()
        >>> n['a'] == '1'
        True

        >>> n['spam'].append(3)
        >>> n['spam']
        [0, 1, 2, 3]

        >>> c['spam']
        [0, 1, 2]

        >>> n['spam'] is c['spam']
        False

        >>> n['spam'] == c['spam']
        False
        """
        return dict(self.items())

    def has_key(self, key):
        """ D.has_key(k) -> True if D has a key k, else False

        >>> c = ConfigFile()
        >>> c.has_key('spam')
        False

        >>> c = ConfigFile(testini())
        >>> c.has_key('spam')
        True
        """
        return key in self.keys()

    __contains__ = has_key

    def clear(self):
        """ D.clear() -> None.  Remove all items from D.

        >>> c = ConfigFile(testini())
        >>> c.clear()
        >>> c
        {}
        """
        for key in self.keys():
            del(self[key])

    def setdefault(self, key, default=None):
        """ D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D

        >>> c = ConfigFile(testini())
        >>> c.setdefault('a', 42)
        '1'

        >>> c.setdefault('ham', 3)
        3

        >>> c.setdefault('ham', 4)
        3

        >>> c.setdefault('green')
        >>> print c['green']
        None
        """
        try:
            return self[key]
        except (KeyError, ):
            self[key] = default
            return default

    def pop(self, key, default=marker):
        """
        D.pop(k[,d]) -> v, remove specified key and return the corresponding value
        If key is not found, d is returned if given, otherwise KeyError is raised


        >>> c = ConfigFile(testini())
        >>> c.pop('spam')
        [0, 1, 2]

        >>> 'spam' in c
        False

        >>> c.pop('eggs', 0)
        0

        >>> c.pop('eggs')
        Traceback (most recent call last):
        ...
        KeyError: 'eggs'
        """
        try:
            value = self[key]
        except (KeyError, ):
            if default is marker:
                raise KeyError(key)
            return default
        else:
            del(self[key])
            return value

    def popitem(self):
        """
        D.popitem() -> (k, v), remove and return some (key, value) pair as a
        2-tuple; but raise KeyError if D is empty

        >>> c = ConfigFile()
        >>> c['eggs'] = 12
        >>> c.popitem()
        ('eggs', 12)

        >>> 'eggs' in c
        False

        The json encoding converts tuples to lists:

        >>> c['a'] = (0, 1)
        >>> c.popitem()
        ('a', [0, 1])
        """
        try:
            kv = self.items()[0]
        except (IndexError, ):
            raise KeyError('popitem(): object is empty')
        del(self[kv[0]])
        return kv

    def update(self, items=None, **extras):
        """
        D.update(E, **F) -> None.  Update D from E and F: for k in E: D[k] = E[k]
        (if E has keys else: for (k, v) in E: D[k] = v) then: for k in F: D[k] = F[k]

        >>> c = ConfigFile()
        >>> c['a'] = 1
        >>> c['a']
        1

        >>> c.update({'a':2, 'b':3})
        >>> c['a']
        2
        >>> c['b']
        3

        >>> c.update(b=4, d=10)
        >>> c['b']
        4
        """
        source = {}
        if items:
            source.update(items)
        source.update(extras)
        for key, value in source.items():
            self[key] = value
